#include "xmlrpcbase64.h"
#include "xmlrpcparser.h"

#include "QPointer"
#include <QtCore/QDateTime>
#include <QtCore/QDebug>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtXml/QDomDocument>

namespace XmlRpc {

class XmlRpcParserContentsPrivate {
public:
    XmlRpcParserContentsPrivate() : isFault(false), faultCode(0), faultString("")
    {
    }

    bool isFault;
    int faultCode;
    QString faultString;
    QVariant params;
};

XmlRpcParserContents::XmlRpcParserContents() : d(new XmlRpcParserContentsPrivate())
{
}

XmlRpcParserContents::~XmlRpcParserContents()
{
    delete d;
}

QVariant XmlRpcParserContents::params() const
{
    return d->params;
}

bool XmlRpcParserContents::isFault() const
{
    return d->isFault;
}

int XmlRpcParserContents::faultCode() const
{
    return d->faultCode;
}

QString XmlRpcParserContents::faultString() const
{
    return d->faultString;
}

void XmlRpcParserContents::setFault(int faultCode, const QString &faultString)
{
    d->isFault = true;
    d->faultCode = faultCode;
    d->faultString = faultString;
}

void XmlRpcParserContents::setParams(const QVariant &params)
{
    d->params = params;
}

XmlRpcParser::XmlRpcParser()
{
    m_valueMap.insert("string", QVariant::String);
    m_valueMap.insert("int", QVariant::Int);
    m_valueMap.insert("i4", QVariant::Int);
    m_valueMap.insert("boolean", QVariant::Bool);
    m_valueMap.insert("double", QVariant::Double);
    m_valueMap.insert("dateTime.iso8601", QVariant::DateTime);
    m_valueMap.insert("array", QVariant::List);
    m_valueMap.insert("struct", QVariant::Map);
    // Make sure nothing else maps to UserType
    m_valueMap.insert("base64", QVariant::UserType);
}

static QDomElement writeElement(const QVariant &var, QDomDocument &doc)
{
    QDomElement elem, data;
    /*
     * See http://www.xmlrpc.com/spec
     * "Natively" supported QVariantTypes: Map (struct), StringList/List (array),
     * Double, Int, Bool, Datetime, String and RpcBase64. Rest will be embedded
     * as strings using QVariant::toString().
     */
    switch (var.type()) {
    case QVariant::Map: {
            elem = doc.createElement("struct");
            QVariantMap tmp = var.toMap();
            QDomElement member, value, name;
            foreach (QString key, tmp.keys()) {
                name = doc.createElement("name");
                value = doc.createElement("value");
                member  = doc.createElement("member");

                elem.appendChild(member);
                member.appendChild(name);
                name.appendChild(doc.createTextNode(key));
                member.appendChild(value);
                value.appendChild(writeElement(tmp.value(key), doc));
            }
        }
        break;
    case QVariant::StringList: // fall through
    case QVariant::List: {
            elem = doc.createElement("array");
            data = doc.createElement("data");
            elem.appendChild(data);
            QList<QVariant> tmp = var.toList();
            QList<QVariant>::const_iterator i;
            QDomElement value;
            for (i = tmp.constBegin(); i != tmp.constEnd(); i++) {
                value = doc.createElement("value");
                data.appendChild(value);
                value.appendChild(writeElement(*i, doc));
            }
        }
        break;
    case QVariant::Double:
        elem = doc.createElement("double");
        elem.appendChild(doc.createTextNode(var.toString()));
        break;
    case QVariant::Int:
        elem = doc.createElement("int");
        elem.appendChild(doc.createTextNode(var.toString()));
        break;
    case QVariant::Bool:
        elem = doc.createElement("boolean");
        elem.appendChild(doc.createTextNode(var.toBool() ? "1" : "0"));
        break;
    case QVariant::DateTime:
        elem = doc.createElement("dateTime.iso8601");
        elem.appendChild(doc.createTextNode(var.toDateTime().toString("yyyyMMddThh:mm:ss")));
        break;
    case QVariant::String:
        elem = doc.createElement("string");
        elem.appendChild(doc.createTextNode(var.toString()));
        break;
    case QVariant::UserType:
        if (qVariantCanConvert<XmlRpcBase64>(var)) {
            XmlRpcBase64 base64 = qVariantValue<XmlRpcBase64>(var);
            elem = doc.createElement("base64");
            elem.appendChild(doc.createTextNode(base64.encoded()));
            break;
        }
        // Fall through
    default:
        // Just try to insert it as text
        elem = doc.createElement("string");
        elem.appendChild(doc.createTextNode(var.toString()));
        break;
    }
    return elem;
}

QString XmlRpcParser::parseToString(const QList<QVariant> &params)
{
    if (params.isEmpty())
            return QString();

    QDomDocument doc;
    QDomElement paramsElement = doc.createElement("params");
    doc.appendChild(paramsElement);
    QDomElement value, param;
    param = doc.createElement("param");
    QList<QVariant>::const_iterator i = params.constBegin();
    while (i != params.constEnd()) {
        paramsElement.appendChild(param);
        value = doc.createElement("value");
        param.appendChild(value);
        value.appendChild(writeElement(*i, doc));
        ++i;
    }
    return doc.toString(4);
}

XmlRpcParserContents* XmlRpcParser::parseRpcResponse(const QString &string)
{
    XmlRpcParserContents* contents = new XmlRpcParserContents;
    QDomDocument doc;
    QString errorMsg;
    int errorLine, errorCol;
    if (!doc.setContent(string, &errorMsg, &errorLine, &errorCol)) {
        contents->setFault(1, QString("Invalid XML document: %1").arg(errorMsg));
        return contents;
    }
    QDomElement root = doc.documentElement();

    // Let's make sure this is a well-formed XML-RPC response.
    if (root.tagName() != "methodResponse"
            || root.childNodes().count() != 1) {
        contents->setFault(-4, "Invalid XML-RPC response");
        return contents;
    }
    QDomElement level1 = root.firstChild().toElement();
    if (level1.tagName() == "fault") {
        // Content looks good but the response is an XML-RPC fault.
        int errorCode;
        QString errorString;
        bool ok = parseError(level1, &errorCode, &errorString);
        if (ok)
            contents->setFault(errorCode, errorString);
        else
            contents->setFault(-2, "XML-RPC fault occured; furthermore, the XML-RPC response "
                              "content was malformed.");
    } else if (level1.tagName() == "params") {
        QVariant param = parseParam(level1.firstChildElement("param"));
        if (param == QVariant())
            contents->setFault(-6, "XML-RPC failed to parse methodResponse parameters");
        else
            contents->setParams(param);
    } else {
        // methodResponse *must* contain either a fault or params
        contents->setFault(-3, "Invalid XML-RPC response (bad content)");
    }
    return contents;
}

QVariant XmlRpcParser::parseParam(const QDomElement &paramRoot)
{
    if (paramRoot.isNull() || paramRoot.tagName() != "param") {
        return QVariant();
    }
    QDomElement value = paramRoot.firstChildElement("value");
    // a value element must be present and have children.
    if (value.isNull() && !value.hasChildNodes())
        return QVariant();
    return parseValue(value.firstChild().toElement());
}

/* XXX: rename or handle <value> tag here? */
QVariant XmlRpcParser::parseValue(const QDomElement &element)
{
    if (element.isNull())
        return QVariant();

    QVariant val = QVariant();
    if (!m_valueMap.contains(element.tagName()))
        return val;
    QVariant::Type type = m_valueMap.value(element.tagName());
    switch (type) {
    case QVariant::Map:
        val = parseStruct(element);
        break;
    case QVariant::List:
        val = parseList(element);
        break;
    case QVariant::Double:
        val = element.text().toFloat();
        break;
    case QVariant::Int:
        val = element.text().toInt();
        break;
    case QVariant::Bool:
        val = element.text().toInt() == 1;
        break;
    case QVariant::DateTime: {
            QDateTime date = QDateTime::fromString(element.text(), "yyyyMMddThh:mm:ss");
            if (date.isValid()) {
                val = date;
            } else {
                qWarning("XML-RPC Bad datetime value");
                val = QDateTime();
            }
        }
        break;
    case QVariant::String:
        val = element.text();
        break;
    case QVariant::UserType:
        // Base64
        val =  qVariantFromValue(XmlRpcBase64(element.text().toAscii()));
        break;
    default:
        qWarning("Bug! Unknown type");
        break;
    }
    return val;
}

/*
 * Parse an XML-RPC struct into a map. The QVariant values may contain
 * embedded arrays and structs, in addition to plain values.
 */
QMap<QString, QVariant> XmlRpcParser::parseStruct(const QDomElement &structElement)
{
    QMap<QString, QVariant> ret;
    if (structElement.tagName() != "struct" || !structElement.hasChildNodes())
        return ret;
    QDomElement memb = structElement.firstChildElement("member");
    while (!memb.isNull()) {
        QDomElement name = memb.firstChildElement("name");
        QDomElement value = memb.firstChildElement("value");
        if (name.isNull() || value.isNull()) {
            qWarning() << "XML-RPC struct: missing name or value";
            continue;
        }
        // Make sure the value is good
        if (value.hasChildNodes())
            ret.insert(name.text(), parseValue(value.firstChild().toElement()));

        memb = memb.nextSiblingElement("member");
    }
    return ret;
}

QList<QVariant> XmlRpcParser::parseList(const QDomElement &arrayElement)
{
    QList<QVariant> ret;

    QDomElement data = arrayElement.firstChildElement("data");
    if (data.isNull())
        return ret;
    QDomElement value = data.firstChildElement("value");
    while (!value.isNull()) {
        ret << parseValue(value.firstChild().toElement());
        value = value.nextSiblingElement("value");
    }
    return ret;
}

/*
 * Parse a fault from the XML-RPC server. If the fault XML is invalid, returns
 * false. Otherwise errorCode and errorString are set to the extracted values.
 */
bool XmlRpcParser::parseError(const QDomElement &paramRoot, int *errorCode, QString *errorString)
{
    if (paramRoot.hasChildNodes() && paramRoot.firstChild().toElement().tagName() == "value") {
        QDomElement value = paramRoot.firstChild().toElement();
        if (!value.hasChildNodes())
            return false;
        QMap<QString, QVariant> faultStruct = parseStruct(value.firstChild().toElement());
        if (faultStruct.size() != 2 || !faultStruct.contains("faultCode")
                || !faultStruct.contains("faultString"))
            return false;
        if (errorCode)
            *errorCode = faultStruct.value("faultCode").toInt();
        if (errorString)
            *errorString = faultStruct.value("faultString").toString();
        return true;
    }
    return false;
}

} // namespace XmlRpc

